package be.panako.cli;


import be.panako.util.Config;
import be.panako.util.Key;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static be.panako.cli.Video.Match;

/**
 * A command line application to make clips based on scan results
 */

@SuppressWarnings("unused")
public class Clip extends Application {
    private static final Logger LOG = Logger.getLogger(Clip.class.getName());
    static DateTimeFormatter srtf = DateTimeFormatter.ofPattern("HH:mm:ss,SSS");
    @Parameter(names = "-log", converter = Video.LevelConverter.class)
    private Level logLevel = Level.WARNING;
    @Parameter(names = "-mask")
    private String mask;
    @Parameter(names = "-pad")
    private int padding;
    @Parameter(names = "-merge")
    private int merge;
    @Parameter(names = "-out")
    private String outdir = ".";
    @Parameter(names = "-in")
    private String indir = ".";
    @Parameter(names = "-filter")
    private String filter;
    @Parameter(names = "-min")
    private int min;
    @Parameter(arity = 1)
    private String infile;
    private ExecutorService runners, batches;

    private static void runFfmpeg(String args) throws IOException, InterruptedException {
        String env = Config.get(Key.DECODER_PIPE_ENVIRONMENT);
        String arg = Config.get(Key.DECODER_PIPE_ENVIRONMENT_ARG);

        Process pr = new ProcessBuilder()
                .command(env, arg, "ffmpeg " + args)
                .redirectErrorStream(true)
                .start();

        try (Reader rdr = new InputStreamReader(pr.getInputStream())) {
            StringBuilder builder = new StringBuilder();
            char[] buf = new char[1024];
            int num;
            while ((num = rdr.read(buf)) != -1) {
                builder.append(buf, 0, num);
            }
            LOG.fine(builder::toString);

            int res = pr.waitFor();
            if (res != 0)
                throw new IOException("failed to execute ffmpeg:\n" + builder);
        }
    }

    public static String str(double time) {
        long secs = (long) time;
        int msec = (int) ((time - secs) * 1E9d);
        return LocalDateTime.ofEpochSecond(secs, msec, ZoneOffset.UTC).format(srtf);
    }

    @Override
    public void run(String... args) {
        try {
            JCommander.newBuilder()
                    .addObject(this)
                    .build()
                    .parse(args);

            Panako.setLevel(logLevel);
            Path inp = Path.of(infile);
            if (!Files.exists(inp)) {
                LOG.severe(() -> "File not found - " + infile);
                return;
            }

            int cpus = availableProcessors();
            runners = Executors.newFixedThreadPool(cpus);
            batches = Executors.newFixedThreadPool(cpus);
            Pattern matpat = filter == null ? null : Pattern.compile(filter);
            try {
                List<Match> batch = new ArrayList<>();
                List<String> scan = Files.readAllLines(inp);
                List<Match> all = scan.stream().map(Match::of).sorted().toList();
                Match prev = new Match();

                for (Match m : all) {
                    if (matpat != null) {
                        Matcher x = matpat.matcher(m.filename);
                        if (!x.find())
                            continue;
                        if (x.groupCount() > 0)
                            m.filename = x.group(1);
                    }
                    if (m.matchName.equals(prev.matchName)) {
                        if (m.filename.equals(prev.filename) && (prev.endsAt + merge >= m.startsAt)) {
                            prev.endsAt = Math.max(prev.endsAt, m.endsAt);
                            continue;
                        }
                    } else {
                        if (prev.filename != null)
                            enqueue(batch, prev.matchName);
                        batch = new ArrayList<>();
                    }
                    batch.add(m);
                    prev = m;
                }
                if (prev.filename != null)
                    enqueue(batch, prev.matchName);

                batches.shutdown();
                if (!batches.awaitTermination(1, TimeUnit.DAYS))
                    LOG.info("timeout");

            } finally {
                runners.shutdownNow();
                if (!runners.awaitTermination(1, TimeUnit.MINUTES))
                    LOG.warning("failed to stop runners");
            }
        } catch (Exception ex) {
            LOG.severe(ex.getMessage());
        }
    }

    private void enqueue(List<Match> batch, String outputName) {
        BatchJob job = new BatchJob(batch, outputName);
        batches.submit(job);
    }

    @Override
    public String description() {
        return "make video clips based on scan results";
    }

    @Override
    public String synopsis() {
        return "clip";
    }

    @Override
    public boolean needsStorage() {
        return true;
    }

    @Override
    public boolean writesToStorage() {
        return false;
    }

    static class ExtractJob implements Callable<Object> {
        private final Match match;
        private final Path infile;
        private final Path tempfile;

        public ExtractJob(Match match, Path source) {
            this.match = match;
            this.infile = source;
            try {
                tempfile = Files.createTempFile("part", ".mkv");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public Object call() {
            LOG.info(() -> "  Handling " + match);
            String cmd = String.format("-y -fflags +genpts -i \"%s\" -map 0:v -map 0:a -c copy -ss %s -t %s \"%s\"",
                    infile, match.start(), match.length(), tempfile);
            try {
                runFfmpeg(cmd);
            } catch (Exception e) {
                LOG.severe(e.getMessage());
            }
            return null;
        }
    }

    class BatchJob implements Runnable {
        private final String outputName;
        private List<Match> batch;

        public BatchJob(List<Match> batch, String outputName) {
            this.batch = batch;
            this.outputName = outputName;
        }

        private Path makeSrt() throws IOException {
            int n = 0;
            double time = 0;
            String txt = "";
            String crlf = "";
            for (Match m : batch) {
                txt = String.format("%s%s%d\n%s --> %s\n<font color=\"#FFFF00\"><b>%s</b></font>\n%s\n",
                        txt, crlf, ++n, str(time), str(time + 5), m.filename, m.start());
                time += m.endsAt - m.startsAt;
                crlf = "\n";
            }
            Path p = Files.createTempFile("all-", ".srt");
            Files.writeString(p, txt);
            return p;
        }

        private void processBatch() throws IOException, InterruptedException {
            batch = batch.stream().filter(m -> m.endsAt - m.startsAt > min).toList();
            if (batch.isEmpty())
                return;

            List<ExtractJob> jobs = new ArrayList<>();
            LOG.warning(() -> outputName + " ..");

            try {
                for (Match x : batch) {
                    x.pad(padding);
                    String fn = (mask != null) ? mask.replace("{}", x.filename) : x.filename;
                    ExtractJob job = new ExtractJob(x, Path.of(indir, fn));
                    jobs.add(job);
                }
                runners.invokeAll(jobs);
                Path all = Files.createTempFile("all-", ".txt");
                Files.write(all, jobs.stream().map(x -> "file '" + x.tempfile + "'").toList());

                Path output = Path.of(outdir, outputName);
                Path pre = Files.createTempFile("all-", ".mkv");
                Path srt = makeSrt();
                String cmd = String.format("-y -f concat -safe 0 -i \"%s\" -c copy \"%s\"", all, pre);
                runFfmpeg(cmd);

                cmd = String.format("-y -i \"%s\" -i \"%s\" -c copy -c:s mov_text -metadata:s:s:0 language=eng \"%s\".mp4",
                        pre, srt, output);
                runFfmpeg(cmd);

                Files.delete(pre);
                Files.delete(srt);
                Files.delete(all);
            } finally {
                for (ExtractJob j : jobs)
                    Files.delete(j.tempfile);
            }
        }

        @Override
        public void run() {
            try {
                processBatch();
            } catch (Exception ex) {
                LOG.severe(ex.getMessage());
            }
        }
    }

}
